package org.bgee.psp4jdbc.jdbcapi;

import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bgee.psp4jdbc.Properties;

/**
 * This class is an implementation of <code>java.sql.Driver</code> that provides 
 * {@link Connection}s that hold a <code>PreparedStatement</code> pool, and 
 * wrapping a real <code>java.sql.Connection</code> obtained from the real underlying 
 * <code>java.sql.Driver</code>.
 * <p>
 * It is recommended to provide the class name of the real underlying 
 * <code>java.sql.Driver</code> via URL or <code>java.util.Properties</code> 
 * when calling {@link #connect(String, java.util.Properties)}
 * (see {@link org.bgee.psp4jdbc.Properties#DRIVERCLASSNAMEKEY} to get the key name). 
 * Otherwise, it means that it is the responsibility of the user to load 
 * the <code>java.sql.Driver</code> 
 * (using for instance <code>Class.forName("my.real.jdbc.driver")</(code>).
 * <p>
 * It is a modification of the class originally developed for log4jdbc by Arthur Blake. 
 *
 * @author Arthur Blake
 * @author Frederic Bastian
 * @author Mathieu Seppey
 */
public class Driver implements java.sql.Driver 
{
    /**
     * <code>Logger</code> of the class. 
     */
    private final static Logger log = LogManager.getLogger(Driver.class.getName());
    /**
     * The underlying driver that was requested by providing its class name.
     */
    private java.sql.Driver realDriver;

    /**
     * Default constructor.
     */
    public Driver()
    {
    }

    /**
     * Static initializer.
     */
    static
    {
        log.info("psp4jdbc Driver registration to DriverManager...");

        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException s) {
            // this exception should never be thrown, JDBC just defines it
            // for completeness
            throw (RuntimeException) new RuntimeException
            ("could not register psp4jdbc driver!").initCause(s);
        }

        log.info("psp4jdbc Driver registration done.");
    }

    /**
     * Given a psp4jdbc type URL, find the real underlying driver
     * that accepts the URL.
     *
     * @param url JDBC connection URL.
     *
     * @return Underlying driver for the given URL. Null is returned if the URL is
     *         not a psp4jdbc type URL or there is no underlying
     *         driver that accepts the URL.
     *
     * @throws SQLException if a database access error occurs.
     */
    private void loadRealDriver(String url, java.util.Properties info) throws SQLException
    {
    	log.entry(url, info);

    	String realUrl = this.getRealUrl(url);
    	
    	if (this.getRealDriver() != null && 
    			this.getRealDriver().acceptsURL(realUrl)) {
    		log.exit(); return;
    	}
    	
    	Properties properties = new Properties();
    	if (!properties.loadProperties(url, info)) {
    		this.setRealDriver(null);
    		throw log.throwing(new SQLException("psp4jdbc does not accept the URL " + url)); 
    	}
    	
    	//register the real Driver and get it from the DriverManager
    	log.info("Trying to load Driver {} using URL {} and properties {} ...", 
    			properties.getDriverClassName(),  realUrl, info); 
    	try {
    		if (properties.getDriverClassName() != null && 
    				properties.getDriverClassName().length() > 0) {
    			Class.forName(properties.getDriverClassName(), true, 
    					Driver.class.getClassLoader()).newInstance();
    		}
	    	this.setRealDriver(DriverManager.getDriver(realUrl));
		} catch (Exception cn) {
			if (cn instanceof SQLException) {
				throw log.throwing((SQLException) cn);
			}
            throw log.throwing(new SQLException(cn));
        }
    	if (this.getRealDriver() != null) {
    		log.info("Driver loaded succesfully, Driver obtained: {}", 
    				this.getRealDriver());
    	} else {
    		throw log.throwing(new SQLException("Could not obtain a valid JDBC Driver"));
    	}
    }


    /**
     * @return the realDriver
     * @see #realDriver
     */
    private java.sql.Driver getRealDriver() {
        return this.realDriver;
    }
    /**
     * @param driver the realDriver to set
     * @see #realDriver
     */
    private void setRealDriver(
            java.sql.Driver driver) {
        this.realDriver = driver;
    }

    /**
     * Get a <code>String</code> representing the real URL that the underlying driver expects 
     * (strip off {@link Properties#URLPREFIX} from the start of the URL, remove psp4jdbc 
     * parameters).
     * 
     * @param url 	a <code>String</code> representing an URL formatted for this driver  
     * 				(should start with {@link Properties#URLPREFIX}, psp4jdbc parameters removed) 
     * @return 		a <code>String</code> representing the real URL that the
     *              underlying driver expects
     */
    private String getRealUrl(String url)
    {
    	log.entry(url);
    	int urlPrefixLength = Properties.URLPREFIX.length();
    	if (url == null || url.length() < urlPrefixLength) {
    		return log.exit(url);
    	}
    	url = url.substring(urlPrefixLength);
    	Pattern pattern = Pattern.compile("( *(?:(?:" + 
                Pattern.quote(Properties.DRIVERCLASSNAMEKEY) + ")|(?:" + 
                Pattern.quote(Properties.GLOBALMAXSIZEKEY) + ")|(?:" + 
                Pattern.quote(Properties.POOLMAXSIZEKEY) + ")|(?:" + 
                Pattern.quote(Properties.EVICTIONMETHODKEY) + ")|(?:" + 
                Pattern.quote(Properties.EVICTIONFACTORKEY) + ")) *(?:= *.*?)?)(&|/|#|$)");
    	Matcher matcher = pattern.matcher(url);
        StringBuffer sb = new StringBuffer();
    	while (matcher.find()) {
    		String replacement = matcher.group(2);
    		if (replacement == null || "&".equals(replacement)) {
    			replacement = "";
    		}
		    matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
    	}
    	matcher.appendTail(sb);
    	
        return log.exit(sb.toString());
    }


    //*********************************************************
    //	IMPLEMENTS Driver METHODS HERE
    //*********************************************************

    /**
     * Returns true if this is a correct URL for psp4jdb.
     *
     * @param url JDBC URL.
     * @return true if this Driver can handle the URL.
     * @throws SQLException to conform to the API
     */
    @Override
    public boolean acceptsURL(String url) 
    {
    	log.entry(url);
    	//we do not check here whether we can load the real underlying Driver 
    	//of if we have a Driver class name provided, as it could be provided 
    	//via java.util.Properties
        return log.exit((new Properties()).loadProperties(url, null));
    }

    @Override
    public Connection connect(String url, java.util.Properties info) throws SQLException {
    	log.entry(url, info);
    	
    	this.loadRealDriver(url, info);
    	Properties properties = new Properties();
    	properties.loadProperties(url, info);

        return new Connection(this.getRealDriver().connect(this.getRealUrl(url), info), 
        		properties);
    }

    /**
     * Get the major version of the driver.  This call will be delegated to the
     * underlying driver (if there is no underlying
     * driver found, then 1 will be returned.)
     *
     * @return the major version of the JDBC driver.
     */
    @Override
    public int getMajorVersion()
    {
        if (this.getRealDriver() == null) {
            return 1;
        } 
        return this.getRealDriver().getMajorVersion();
    }

    /**
     * Get the minor version of the driver.  This call will be delegated to the
     * underlying driver (if there is no underlying
     * driver found, then 0 will be returned.)
     *
     * @return the minor version of the JDBC driver.
     */
    @Override
    public int getMinorVersion()
    {
        if (this.getRealDriver() == null) {
            return 0; 
        } 
        return this.getRealDriver().getMinorVersion();
    }

    /**
     * Gets information about the possible properties for the underlying driver.
     *
     * @param url  the URL of the database to which to connect
     *
     * @param info a proposed list of tag/value pairs that will be sent on
     *             connect open
     * @return     an array of <code>DriverPropertyInfo</code> objects describing
     *             possible properties.  This array may be an empty array if no
     *             properties are required, or if no underlying driver was found.
     *
     * @throws SQLException if a database access error occurs
     */
    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
            throws SQLException {
    	log.entry(url, info);
    	
    	Properties properties = new Properties();
    	properties.loadProperties(url, info);
   
    	//TODO: use introspection
   		DriverPropertyInfo driverNameProp = new DriverPropertyInfo(
   				Properties.DRIVERCLASSNAMEKEY, properties.getDriverClassName()); 
   		driverNameProp.required = true;
   		driverNameProp.description = "Class name of the real underlyng Driver to use, " +
   				"for instance, com.mysql.jdbc.Driver"; 
   		
   		DriverPropertyInfo maxSizeProp = new DriverPropertyInfo(
   				Properties.GLOBALMAXSIZEKEY, String.valueOf(Properties.getGlobalMaxSize())); 
   		maxSizeProp.required = false;
   		maxSizeProp.description = "Global maximum size of the pools of the application. " +
   				"Default is " + Properties.DEFAULTGLOBALMAXSIZE; 
   		
   		DriverPropertyInfo poolSizeProp = new DriverPropertyInfo(
   				Properties.POOLMAXSIZEKEY, String.valueOf(properties.getPoolMaxSize())); 
   		poolSizeProp.required = false;
   		poolSizeProp.description = "Maximum size of the pool of individual connections. " +
   				"Default is " + Properties.DEFAULTPOOLMAXSIZE; 
   		
   		DriverPropertyInfo evictionProp = new DriverPropertyInfo(
   				Properties.EVICTIONMETHODKEY, properties.getEvictionMethod().toString()); 
   		evictionProp.required = false;
   		evictionProp.description = "Eviction method to use when maximum size reached. " +
   				"Possible choices are LRU and LFU. Default is " + 
   				Properties.DEFAULTEVICTIONMETHOD; 
   		
   		DriverPropertyInfo evictionFactorProp = new DriverPropertyInfo(
   				Properties.EVICTIONFACTORKEY, Float.toString(properties.getEvictionFactor())); 
   		evictionProp.required = false;
   		evictionProp.description = "Define the proportion of elements in a pool " +
   				"to be removed when the eviction method is triggered.. Default is " + 
   				Properties.DEFAULTEVICTIONFACTOR; 
   
   
   		DriverPropertyInfo[] pspProps = null;
   		int index = 0;
   		//number of psp4jdbc property infos, if you add some, modify this length.
   		//TODO: not needed anymore if introspection is implemented
   		int length = 5;
        this.loadRealDriver(url, info);
        if (this.getRealDriver() != null) {
            DriverPropertyInfo[] driverProps = 
            		this.getRealDriver().getPropertyInfo(this.getRealUrl(url), info);
            if (driverProps != null && driverProps.length > 0) {
            	index = driverProps.length;
            	length += driverProps.length;
            	pspProps = Arrays.copyOf(driverProps, length);
            } 
        } 
        if (pspProps == null) {
        	pspProps = new DriverPropertyInfo[length];
        }
        pspProps[index + 0] = driverNameProp;
        pspProps[index + 1] = maxSizeProp;
        pspProps[index + 2] = poolSizeProp;
        pspProps[index + 3] = evictionProp;
        pspProps[index + 4] = evictionFactorProp;

        return log.exit(pspProps);
    }

    /**
     * Report whether the underlying driver is JDBC compliant.  If there is no
     * underlying driver, false will be returned, because the driver cannot
     * actually do any work without an underlying driver.
     *
     * @return <code>true</code> if the underlying driver is JDBC Compliant;
     *         <code>false</code> otherwise.
     */
    @Override
    public boolean jdbcCompliant()
    {
        return this.getRealDriver() != null &&
                this.getRealDriver().jdbcCompliant();
    }

    @Override
    public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException{

        if (this.getRealDriver() != null) {
        	return this.getRealDriver().getParentLogger();
        }
    	return null;  

    }

}
